O que você vai aprender nesta aula?
Após o término da aula você terá aprendido:
Vamos começar com uma rápida revisão dos conceitos de Orientação a Objetos e como eles são aplicados em Python.
O paradigma de programação orientada a objetos tem por objetivo fornecer uma abstração do mundo real e aplicá-la para a programação.
Objetos são componentes de software que incluem dados e comportamentos. Por exemplo cães possuem estados (nome, raça e cor) e comportamentos (latir, abanar o rabo e pegar objetos). Já bicicletas possuem outros estados (modelo, marcha atual e velocidade atual) e comportamentos (mudar marcha, frear).
Outro conceito importante é o de classe. Estas representam a estrutura de um objeto, por exemplo: a receita de um bolo. Nesse exemplo a receita seria a classe que contém instruções de como criar o objeto, além de ter informações sobre a instância (bolo).
Em Python os objetos possuem atributos que podem ser tanto métodos (funções vinculadas ao objeto) ou atributos de dados do objeto. Este último geralmente é geralmente chamado de atributo.
É importante saber que em Python instâncias de classes são chamadas exatamente de instâncias. É comum em Java/C++ chamar "instâncias de classes" de "objetos de classes". Isso não acontece em Python, pois nesta linguagem tudo é um objeto e, portanto, chamar a instância de uma classe de objeto é redundante.
Quando falamos sobre linguagens que implementam o paradigma de orientação a objetos elas devem fornecer quatro conceitos básicos:
In [62]:
notas = {'bia': 10, 'pedro': 0, 'ana': 7}
notas
Out[62]:
O dicionários possui diversos métodos que usamos para alterar os objetos:
In [63]:
notas.keys()
Out[63]:
In [64]:
notas.pop('bia')
Out[64]:
In [65]:
notas
Out[65]:
Podemos usar a função dir()
para inspecionar os métodos e atributos do dict
notas:
In [66]:
dir(notas)
Out[66]:
Aqui vemos vários métodos que o nome contém underscores no começo e fim como __len__
, __getitem__
, __setitem__
. Esses métodos são chamados de métodos especiais que fazem parte do modelo de dados do Python. Esses métodos são chamados pelo interpretador quando uma sintaxe especial é acionada. Como, por exemplo, quando acessamos os itens do dicionário por sua chave o interpretador invoca a função dict.__getitem__()
:
In [67]:
notas
Out[67]:
In [68]:
notas.__getitem__('ana')
Out[68]:
In [69]:
notas['ana']
Out[69]:
In [70]:
notas.__getitem__('joselito')
In [71]:
notas['joselito']
O dict
também possui atributos de dados especiais como __class__
, que armazena o nome da classe do objeto, e __doc__
que retém a docstring do objeto:
In [72]:
notas.__class__
Out[72]:
In [73]:
notas.__doc__
Out[73]:
Para ver a docstring formatada para saída use a função print()
:
In [74]:
print(notas.__doc__)
Números são objetos:
In [75]:
3 + 4
Out[75]:
Possuem métodos e atributos:
In [76]:
print(3 .__doc__)
In [77]:
3 .__add__(4)
Out[77]:
In [78]:
3 .__sub__(4)
Out[78]:
Só lembrando que os métodos especiais não devem ser chamados diretamente, os exemplos anteriores só servem para ilustrar o funcionamento e existência desses métodos. Caso você queira consultar a documentação de um objeto use a função help()
:
In [79]:
help(3)
Como explicado na [py-intro] Aula 05 funções também são objetos. Na terminologia utilizada pelos livros isso quer dizer que, em Python, as funções são objetos de primeira classe ou cidadãos de primeira classe.
In [80]:
def soma(a, b):
""" retorna a + b """
soma = a + b
return soma
In [81]:
soma(1, 2)
Out[81]:
In [82]:
soma
Out[82]:
Podemos a atribuir funções a variáveis:
In [83]:
adição = soma
adição
Out[83]:
Acessar atributos:
In [84]:
adição.__name__
Out[84]:
In [85]:
adição.__doc__
Out[85]:
Podemos ver o bytecode que a função executa usando o módudlo dis
(disassembly), enviando a função soma()
como argumento da função dis.dis()
:
In [86]:
import dis
dis.dis(soma)
In [87]:
"1" + 10
Tentamos concatenar o número 10 à string "1", porém uma exceção do tipo TypeError
foi levantada dizendo que não foi possível converter um objeto int
para str
de forma implicita.
Em Javascript e PHP, linguagens que possuem tipagem fraca, não seria levantado uma exceção e o interpretador faria a conversão de um dos tipos. No Javascript (1.5) o resultado seria uma string "110" e no PHP (5.6) o número 11.
Aqui percebemos que a operação "1" + 10
pode produzir dois resultados: uma string ou um número. Conforme consta no Zen do Python: "In the face of ambiguity, refuse the temptation to guess" (Ao encontrar uma ambiguidade recuse a tentação de inferir) e é exatamente o que o Python faz: a linguagem recusa-se a inferir o tipo do resultado e levanta uma exceção.
Para fazermos esse exemplo funcionar precisamos converter os tipos explicitamente:
In [88]:
"1" + str(10)
Out[88]:
In [89]:
int("1") + 10
Out[89]:
Dizemos que uma linguagem possui tipagem dinâmica quando não é necessário especificar explicitamente os tipos das váriaveis. Os objetos possuem tipos, porém as variáveis podem referenciar objetos de quaisquer tipos. Verificações de tipos são feitas em tempo de execução e não durante a compilação.
Quando definimos uma função dobra(x)
que retorna o valor recebido multiplicado por 2 podemos receber qualquer tipo de objeto como argumento:
In [90]:
def dobra(x):
return x * 2
Podemos dobrar int
:
In [91]:
dobra(2)
Out[91]:
Dobrar float
:
In [92]:
dobra(1.15)
Out[92]:
string
s:
In [93]:
dobra('bo')
Out[93]:
sequências:
In [94]:
dobra([1, 2, 3])
Out[94]:
In [95]:
dobra((4, 5, 6))
Out[95]:
Tipos que não suportam multiplicação por inteiros levantarão exceção quando executados:
In [96]:
dobra(None)
A função type()
nos permite verificar os tipos dos objetos:
In [97]:
type(1)
Out[97]:
In [98]:
type([1, 2, 3])
Out[98]:
In [99]:
type((1, 2, 3))
Out[99]:
In [100]:
type({})
Out[100]:
In [101]:
type('lalala')
Out[101]:
In [102]:
type(False)
Out[102]:
No Python existem objetos mutáveis e imutáveis, já vimos vários exemplos disso ao longo do curso. O estado (atributo) de objetos mutáveis podem ser alterados, já obetos imutáveis não podem ser alterados de forma alguma.
A tabela abaixo mostra a mutabilidade dos tipos embutidos do Python:
Imutáveis | Mutáveis |
---|---|
tuple | list |
números (int, float, complex) | dict |
frozenset | set |
str, bytes | objetos que permitem alteração de atributos por acesso direto, setters ou métodos |
Vamos ver alguns exemplos que demonstram isso:
In [103]:
a = 10
a
Out[103]:
Todo objeto python possui uma identidade, um número único que diferencia esse objeto. Podemos acessar a identidade de um objeto usando a função id()
:
In [104]:
id(a)
Out[104]:
Isso quer dizer que a identidade do objeto a
é 10894368.
Agora vamos tentar mudar o valor de a:
In [105]:
b = 3
b
Out[105]:
In [106]:
a += b
a
Out[106]:
In [107]:
id(a)
Out[107]:
A identidade mudou, isso significa que a variável a
está referenciando outro objeto que foi criado quando executamos a += b
.
Vamos ver agora um exemplo de objeto mutável:
In [108]:
lista = [1, 2, 3, 4]
lista
Out[108]:
Vamos verificar a identidade dessa lista:
In [109]:
id(lista)
Out[109]:
In [110]:
lista.append(10)
lista.remove(2)
lista += [-4, -3]
lista
Out[110]:
In [111]:
id(lista)
Out[111]:
Mesmo modificando a lista através da inserção e remoção de valores sua identidade continua a mesma.
Strings também são imutáveis:
In [112]:
s = 'abcd'
In [113]:
id(s)
Out[113]:
In [114]:
s[0] = 'z'
Como vimos na aula dois do módulo de introdução strings são imutáveis e para alterar seu valor precisamos usar slicing
:
In [115]:
s = 'z' + s[1:]
s
Out[115]:
In [116]:
id(s)
Out[116]:
Comparando a identidade de s
antes e depois da mudança vemos que trata-se de objetos diferentes.
In [117]:
a = [1, 2, 3]
a
Out[117]:
In [118]:
b = a
In [119]:
a.append(4)
In [120]:
b
Out[120]:
As variáveis a
e b
armazenam referências à mesma lista em vez de cópias.
É importante notar que objetos são criados antes da atribuição. A operação do lado direito de uma atribuição ocorre antes que a atribuição:
In [121]:
c = 1 / 0
Como não foi possível criar o número - por representar uma operação inválida (divisão por zero) para a linguagem - a variável c não foi atribuída a nenhum objeto:
In [122]:
c
Como as variáveis são apenas rótulos a forma correta de falar sobre atribuição é "a variável x
foi atribuída à (instância) lâmpada" e não "a lâmpada foi atribuída à variável x
". Pois é como se colocássemos um "post-it" x
em um objeto, e não guardássemos esse objeto em uma caixa x
.
Por serem rótulos podemos atribuir diversos rótulos a um mesmo objeto. Isso faz com que apelidos (aliases) sejam criados:
In [123]:
josé = {'nome': 'José Silva', 'idade': 10}
zé = josé
zé is josé
Out[123]:
In [124]:
id(zé), id(josé)
Out[124]:
In [125]:
zé['ano_nascimento'] = 2006
josé
Out[125]:
Vamos supor que exista um impostor - o João - que possua as mesmas credenciais que o José Silva. Suas credenciais são as mesmas, porém João não é José:
In [126]:
joão = {'nome': 'José Silva', 'idade': 10, 'ano_nascimento': 2006}
joão == josé
Out[126]:
O valor de seus dados (ou credenciais) são iguais, porém eles não são os mesmos:
In [127]:
joão is josé
Out[127]:
Nesse exemplo vimos o apelidamento (aliasing). josé
e zé
são apelidos (aliases): duas variáveis associadas ao mesmo objeto. Por outro lado vimos que joão
não é um apelido de josé
: essas variáveis estão associadas a objetos distintos. O que acontece é que joão
e josé
possuem o mesmo valor - que é isso que ==
compara - mas têm identidades diferentes.
O operador ==
realiza a comparação dos valores de objetos (os dados armazenados por eles), enquanto is
compara suas identidades. É mais comum comparar valores do que identidades, por esse motivo ==
aparece com mais frequência que is
em códigos Python. Um caso em que o is
é bastante utilizada é para comparação com None
:
In [128]:
a = 10
a is None
Out[128]:
In [129]:
b = None
b is None
Out[129]:
In [130]:
class Cão:
qtd_patas = 4
carnívoro = True
nervoso = False
def __init__(self, nome):
self.nome = nome
Na primeira linha definimos uma classe de nome Cão
.
Da segunda até a quarta linha definimos os atributos de classe qtd_patas
, carnívoro
, nervoso
. Os atributos de classe representam dados que aparecem em todas as classes.
Na sexta linha definimos o inicializador (também pode ser chamado de construtor) que deve receber o nome do Cão
.
Na última linha criamos o atributo da instância nome
e associamos à string enviada para o construtor.
Vamos agora criar uma instância de Cão:
In [131]:
rex = Cão('Rex')
type(rex)
Out[131]:
Vamos verificar seus atributos:
In [132]:
rex.qtd_patas
Out[132]:
In [133]:
rex.carnívoro
Out[133]:
In [134]:
rex.nervoso
Out[134]:
In [135]:
rex.nome
Out[135]:
Podemos também alterar esses atributos:
In [136]:
rex.nervoso = True
rex.nervoso
Out[136]:
Mudamos apenas o atributo nervoso
da instância rex
. O valor de Cão.nervoso
continua o mesmo:
In [137]:
Cão.nervoso
Out[137]:
Também podemos criar atributos dinamicamente para nossa instância rex
:
In [138]:
rex.sujo = True
rex.sujo
Out[138]:
In [139]:
rex.idade = 5
rex.idade
Out[139]:
Lembrando mais uma vez que essas mudanças ocorrem somente na instância e não na classe:
In [140]:
Cão.sujo
In [141]:
Cão.idade
Classes também são objetos e podemos acessar seus atributos:
In [142]:
Cão.__name__
Out[142]:
In [143]:
Cão.qtd_patas
Out[143]:
In [144]:
Cão.nervoso
Out[144]:
In [145]:
Cão.carnívoro
Out[145]:
In [146]:
Cão.nome
Não podemos acessar o nome
, pois nome
é um atributo que é associado somente a instâncias da classe.
In [147]:
fido = Cão('Fido')
fido.nome
Out[147]:
Os atributos de classe são usados para fornecerer valores padrão para dados que são compartilhados por todos os "cães" como, por exemplo, a quantidade de patas.
Agora vamos criar métodos (funções associadas a classes) para a classe Cão
:
In [148]:
class Cão:
qtd_patas = 4
carnívoro = True
nervoso = False
def __init__(self, nome):
self.nome = nome
def latir(self, vezes=1):
""" Latir do cão. Quanto mais nervoso mais late. """
vezes += self.nervoso * vezes
latido = 'Au! ' * vezes
print('{}: {}'.format(self.nome, latido))
In [149]:
rex = Cão('Rex')
rex.latir()
In [150]:
rex.nervoso = True
rex.latir()
In [157]:
rex.latir(10)
Vamos brincar um pouco mais com o Cão
e implementar ainda mais métodos:
In [152]:
class Cão:
qtd_patas = 4
carnívoro = True
nervoso = False
def __init__(self, nome, truques=None):
self.nome = nome
if not truques:
self.truques = []
else:
self.truques = list(truques)
def latir(self, vezes=1):
""" Latir do cão. Quanto mais nervoso mais late. """
vezes += self.nervoso * vezes
latido = 'Au! ' * vezes
print('{}: {}'.format(self.nome, latido))
def ensina_truque(self, truque):
if truque not in self.truques:
self.truques.append(truque)
In [153]:
fido = Cão('Fido', truques=['Pegar'])
In [154]:
fido.truques
Out[154]:
In [155]:
fido.ensina_truque('Rolar')
fido.truques
Out[155]:
In [156]:
fido.ensina_truque('Pegar')
fido.truques
Out[156]:
In [13]:
class ExemploInstancia:
def metodo_instancia(self):
print('Recebi {}'.format(self))
Não podemos chamar o método de instância somente com a classe:
In [14]:
ExemploInstancia.metodo_instancia()
Precisamos criar uma instância para utilizá-lo:
In [15]:
inst = ExemploInstancia()
inst.metodo_instancia()
Já os métodos de classe (ou class methods) são métodos referentes à classe como um todo e recebem - não uma instância mas o - objeto da classe.
Para tornar o método de uma classe um classmethod usamos o decorador @classmethod
. Decoradores são usados para "decorar" (ou marcar) funções e modificar seu comportamento de alguma maneira. Na Aula 05 deste módulo (de orientação a objetos em python) falaremos mais sobre decoradores.
Métodos de classe são definidos e utilizados assim:
In [16]:
class ExemploClasse:
@classmethod
def metodo_classe(cls):
print("Recebi {}".format(cls))
Podemos chamar o método usando o objeto de classe ExemploClasse
:
In [17]:
ExemploClasse.metodo_classe()
Também podemos chamar o método a partir de uma instância dessa classe. Por ser um classmethod o método continuará a receber como argumento o objeto da classe e não a instância:
In [18]:
inst = ExemploClasse()
inst.metodo_classe()
Por fim também temos os métodos estáticos que funcionam como funções simples agregadas a objetos ou classes. Eles não recebem argumentos de forma automática:
In [19]:
class Exemplo:
@staticmethod
def metodo_estático():
print('Sou estátio e não recebo nada')
In [20]:
Exemplo.metodo_estático()
Também podemos chamar o método estático a partir de uma instância:
In [21]:
inst = Exemplo()
inst.metodo_estático()
Se for criar um método estático pense bem se esse método realmente precisa estar associado àquela classe. Muitas vezes podemos, ao invés de usar staticmethod, criar uma função e deixá-la associado ao módulo.
Por exemplo, no framework web Django, a autenticação de usuários são feitos com funções e não métodos estáticos da classe do usuário:
from django.contrib.auth import authenticate, login, logout
def exemplo_login_view(request):
user = authenticate('usuario', 'senha')
login(request, user) # usuário é logado no sistema
...
def exemplo_logout_view(request):
logout(request.user) # usuário associado aquela requisição é deslogado do sistema
...